事件循环 Event Loop 面试题全解析
一、核心要点速览
💡 核心考点
- 执行栈: 同步任务的执行环境(LIFO)
- 任务队列: 异步任务的等待队列(FIFO)
- 宏任务: setTimeout、setInterval、I/O 等
- 微任务: Promise.then、MutationObserver 等
- 执行顺序: 同步 → 微任务 → 宏任务
二、JavaScript 运行时环境
1. 单线程模型
javascript
// JavaScript 是单线程语言
// 同一时间只能执行一个任务
console.log('start')
setTimeout(() => {
console.log('timeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise')
})
console.log('end')
// 输出顺序:
// start
// end
// promise
// timeout2. 为什么需要事件循环
┌──────────────────────────────────────────────────────────┐
│ 为什么需要 Event Loop │
└──────────────────────────────────────────────────────────┘
问题场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
如果 JavaScript 没有事件循环:
用户点击按钮
│
▼
┌─────────────────┐
│ 处理点击事件 │ ← 耗时 3 秒
└────────┬────────┘
│
▼
界面卡死 3 秒!
│
▼
❌ 无法响应其他操作
❌ 无法滚动
❌ 无法输入
解决方案:Event Loop
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
用户点击按钮
│
▼
┌─────────────────┐
│ 注册回调函数 │ ← 立即完成
│ 加入任务队列 │
└────────┬────────┘
│
▼
继续响应其他操作 ✓
│
▼
空闲时从队列取出执行 ✓
优势:
✓ 非阻塞 I/O
✓ 高并发能力
✓ 良好的用户体验
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━三、执行上下文与调用栈
1. 执行栈(Call Stack)
javascript
// 执行栈:后进先出(LIFO)
function first() {
second()
}
function second() {
third()
}
function third() {
console.log('Hello')
}
first()
// 调用栈变化:
// ┌─────────────┐
// │ first() │
// └─────────────┘
// ↓
// ┌─────────────┐
// │ second() │
// ├─────────────┤
// │ first() │
// └─────────────┘
// ↓
// ┌─────────────┐
// │ third() │
// ├─────────────┤
// │ second() │
// ├─────────────┤
// │ first() │
// └─────────────┘
// ↓
// 执行完成,全部弹出2. 同步任务执行流程
时间 → ─────────────────────────────────────────────────►
同步代码执行:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
源代码:
console.log('A')
console.log('B')
console.log('C')
执行栈:
┌─────┐
│ A │ → 打印 A
└─────┘
↓
┌─────┐
│ B │ → 打印 B
└─────┘
↓
┌─────┐
│ C │ → 打印 C
└─────┘
↓
空栈
输出:A → B → C
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━四、事件循环机制
1. Event Loop 完整流程
┌──────────────────────────────────────────────────────────┐
│ Event Loop 完整流程 │
└──────────────────────────────────────────────────────────┘
执行流程图:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────┐
│ 执行同步代码 │
│ (调用栈) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 调用栈为空? │
└────────┬────────┘
│
┌─────┴─────┐
│ │
是 否
│ │
▼ │
┌──────────────┐ │
│ 执行所有微任务│ │
│(Microtasks) │ │
└────────┬─────┘ │
│ │
▼ │
┌──────────────┐ │
│ 渲染 DOM │ │
│ (如果需要) │ │
└────────┬─────┘ │
│ │
▼ │
┌──────────────┐ │
│ 执行一个宏任务│ │
│(Macrotask) │ │
└────────┬─────┘ │
│ │
└─────────┘
│
▼
回到开始
无限循环...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━2. 任务队列详解
┌──────────────────────────────────────────────────────────┐
│ 任务队列结构 │
└──────────────────────────────────────────────────────────┘
内存结构:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌────────────────────────────────────────────┐
│ JavaScript 运行时 │
├────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Call Stack │ │
│ │ (调用栈) │ │
│ │ │ │
│ │ │ │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Microtask │ │ Macrotask │ │
│ │ Queue │ │ Queue │ │
│ │ (微任务队列) │ │ (宏任务队列) │ │
│ │ │ │ │ │
│ │ • Promise │ │ • setTimeout │ │
│ │ • Mutation │ │ • setInterval │ │
│ │ • queueMicro │ │ • I/O │ │
│ │ │ │ • UI 渲染 │ │
│ └──────────────┘ └──────────────────┘ │
│ │
└────────────────────────────────────────────┘
执行优先级:
1. 清空调用栈(同步代码)
2. 清空微任务队列
3. 渲染 DOM(如需要)
4. 执行一个宏任务
5. 重复步骤 1-4
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━五、宏任务 vs 微任务
1. 任务类型对比
javascript
// ========== 宏任务(Macrotask)==========
// 1. setTimeout / setInterval
setTimeout(() => {
console.log('timeout')
}, 0)
// 2. setImmediate (Node.js)
setImmediate(() => {
console.log('immediate')
})
// 3. I/O 操作
fs.readFile('file.txt', (err, data) => {
console.log('file read')
})
// 4. UI 渲染
element.addEventListener('click', () => {
console.log('click')
})
// 5. postMessage
window.postMessage('message', '*')
// ========== 微任务(Microtask)==========
// 1. Promise.then / catch / finally
Promise.resolve().then(() => {
console.log('promise then')
})
// 2. queueMicrotask
queueMicrotask(() => {
console.log('microtask')
})
// 3. MutationObserver
const observer = new MutationObserver(() => {
console.log('DOM changed')
})
// 4. process.nextTick (Node.js)
process.nextTick(() => {
console.log('next tick')
})2. 详细对比表
| 特性 | 宏任务(Macrotask) | 微任务(Microtask) |
|---|---|---|
| 包含类型 | setTimeout、setInterval、I/O、UI 渲染 | Promise.then、queueMicrotask、MutationObserver |
| 执行时机 | 每次事件循环执行一个 | 每轮事件循环清空所有 |
| 优先级 | 低 | 高 |
| 产生时间 | 当前宏任务结束后 | 当前任务结束后立即 |
| 数量 | 每次一个 | 一次性全部执行 |
| 典型应用 | 延迟执行、定时器 | 数据更新、DOM 批量修改 |
3. 执行顺序示例
javascript
console.log('script start')
setTimeout(() => {
console.log('setTimeout 1')
setTimeout(() => {
console.log('setTimeout 2')
}, 0)
Promise.resolve().then(() => {
console.log('promise in setTimeout')
})
}, 0)
Promise.resolve().then(() => {
console.log('promise 1')
Promise.resolve().then(() => {
console.log('promise 2')
})
})
console.log('script end')
// 输出顺序:
// 1. script start (同步)
// 2. script end (同步)
// 3. promise 1 (微任务)
// 4. promise 2 (微任务)
// 5. setTimeout 1 (宏任务)
// 6. promise in setTimeout(微任务)
// 7. setTimeout 2 (宏任务)六、经典面试题
题目 1:基础题
javascript
console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('3')
})
console.log('4')
// 输出:1, 4, 3, 2
// 解析:
// 同步:1, 4
// 微任务:3
// 宏任务:2题目 2:嵌套题
javascript
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})
console.log('script end')
// 输出顺序:
// 1. script start
// 2. async1 start
// 3. async2
// 4. promise1
// 5. script end
// 6. async1 end (await 后的代码是微任务)
// 7. promise2
// 8. setTimeout
// 详细解析:
// 同步:script start, async1 start, async2, promise1, script end
// 微任务:async1 end, promise2
// 宏任务:setTimeout题目 3:复杂嵌套
javascript
console.log('A')
setTimeout(() => {
console.log('B')
Promise.resolve().then(() => {
console.log('C')
})
}, 0)
Promise.resolve().then(() => {
console.log('D')
setTimeout(() => {
console.log('E')
}, 0)
})
console.log('F')
// 输出:A, F, D, B, C, E
// 解析:
// 同步:A, F
// 微任务:D
// 宏任务:B
// 微任务:C
// 宏任务:E题目 4:async/await 深入
javascript
async function test() {
console.log('1')
await new Promise(resolve => {
console.log('2')
resolve()
})
console.log('3')
await new Promise(resolve => {
console.log('4')
resolve()
})
console.log('5')
}
test()
console.log('6')
// 输出:1, 2, 6, 3, 4, 5
// 解析:
// 同步:1, 2, 6
// await 将代码分割成多个微任务
// 微任务 1: 3, 4
// 微任务 2: 5七、实际应用
1. 批量 DOM 更新
javascript
// 使用微任务批量更新 DOM
const updates = []
function scheduleUpdate(element, value) {
updates.push({ element, value })
// 使用微任务批量处理
queueMicrotask(() => {
updates.forEach(({ element, value }) => {
element.textContent = value
})
updates.length = 0
})
}
// 多次调用只会触发一次 DOM 更新
scheduleUpdate(el1, 'value1')
scheduleUpdate(el2, 'value2')
scheduleUpdate(el3, 'value3')2. 延迟执行优化
javascript
// 使用 setTimeout 延迟执行
setTimeout(() => {
heavyComputation()
}, 0)
// 使用微任务(更快)
queueMicrotask(() => {
heavyComputation()
})
// 或者
Promise.resolve().then(() => {
heavyComputation()
})
// 性能对比:
// setTimeout: ~4ms 延迟
// queueMicrotask: ~0.1ms 延迟3. 状态同步
javascript
class Store {
constructor() {
this.state = {}
this.listeners = []
this.pending = false
}
setState(newState) {
this.state = { ...this.state, ...newState }
if (!this.pending) {
this.pending = true
// 使用微任务批量通知
queueMicrotask(() => {
this.listeners.forEach(fn => fn(this.state))
this.pending = false
})
}
}
subscribe(fn) {
this.listeners.push(fn)
}
}
// 多次 setState 只触发一次通知
store.setState({ a: 1 })
store.setState({ b: 2 })
store.setState({ c: 3 })
// 监听器只会被调用一次八、浏览器与 Node.js 的差异
1. 执行环境对比
┌──────────────────────────────────────────────────────────┐
│ 浏览器 vs Node.js 的事件循环 │
└──────────────────────────────────────────────────────────┘
浏览器 Event Loop:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────┐
│ Call Stack │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Microtask Queue │
│ • Promise callbacks │
│ • MutationObserver │
│ • queueMicrotask │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Macrotask Queue │
│ • setTimeout/setInterval │
│ • I/O │
│ • UI rendering │
│ • user input events │
└─────────────────────────────────┘
Node.js Event Loop:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────┐
│ Call Stack │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ nextTick Queue │ ← 特殊队列
│ • process.nextTick() │
│ (优先级最高,独立于微任务) │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Microtask Queue │
│ • Promise callbacks │
│ • queueMicrotask │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Macrotask Queues │
│ • timers (setTimeout/setInterval)
│ • pending callbacks (I/O) │
│ • idle, prepare │
│ • poll (I/O) │
│ • check (setImmediate) │
│ • close callbacks │
└─────────────────────────────────┘
关键差异:
✓ Node.js 有 process.nextTick(优先级最高)
✓ Node.js 有多个宏任务队列(分阶段执行)
✓ setImmediate 在 poll 阶段后执行
✓ timers 在 timer 阶段执行
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━2. Node.js 特有行为
javascript
// Node.js 中:process.nextTick 优先级最高
console.log('start')
process.nextTick(() => {
console.log('nextTick 1')
process.nextTick(() => {
console.log('nextTick 2')
})
})
Promise.resolve().then(() => {
console.log('promise')
})
console.log('end')
// Node.js 输出:
// start
// end
// nextTick 1
// nextTick 2
// promise
// 浏览器输出(没有 nextTick):
// start
// end
// promise3. setTimeout vs setImmediate
javascript
// Node.js 中
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
// 结果不确定,取决于执行时的阶段
// 但在 I/O 回调中
const fs = require('fs')
fs.readFile('file.txt', () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// 总是输出:
// immediate
// timeout九、面试标准回答
事件循环(Event Loop)是 JavaScript 处理异步任务的机制,它使得单线程的 JavaScript 能够高效地处理大量并发操作。
核心概念包括:
- 调用栈(Call Stack):执行同步代码,遵循后进先出原则
- 任务队列:存储异步任务的回调,分为宏任务队列和微任务队列
- 事件循环:不断检查调用栈是否为空,然后处理任务队列中的任务
执行流程是:
- 执行所有同步代码(清空调用栈)
- 执行所有微任务(Promise.then、queueMicrotask 等)
- 渲染 DOM(如需要)
- 执行一个宏任务(setTimeout、setInterval 等)
- 重复步骤 1-4
宏任务和微任务的区别:
- 宏任务:setTimeout、setInterval、I/O、UI 渲染,每次事件循环只执行一个
- 微任务:Promise.then、queueMicrotask、MutationObserver,每轮循环清空所有
- 优先级:微任务 > 宏任务
async/await 的执行:
- async 函数立即执行(同步部分)
- await 后面的表达式暂停执行
- await 后的代码作为微任务执行
实际应用中,我会:
- 使用微任务批量处理 DOM 更新(提高性能)
- 使用 setTimeout 实现延迟执行
- 理解 Vue/React 的状态更新机制(基于微任务)
在 Node.js 中,还需要注意 process.nextTick 的特殊性(优先级最高,独立于微任务队列)。
十、记忆口诀
事件循环歌诀:
JS 是单线程,
Event Loop 来处理。
调用栈里放同步,
任务队列存异步!
微任务优先级高,
Promise 和 queueMicro。
宏任务在后面等,
setTimeout 和 I/O!
执行顺序要记牢:
同步代码最先跑,
微任务紧接着,
宏任务最后到!
async/await 特殊:
await 后面是微任务,
暂停恢复自动处理,
异步同步两不误!十一、推荐资源
- MDN Event Loop
- Jake Archibald: In The Loop - 精彩视频讲解
- What the heck is the event loop anyway? - Philip Roberts 经典演讲
- Node.js Event Loop
十二、总结一句话
- Event Loop: 调用栈 + 任务队列 = 异步编程核心机制 🔄
- 宏任务 vs 微任务: 优先级不同 + 执行时机不同 = 正确的执行顺序 ⚡
- async/await: 同步写法 + 微任务实现 = 异步编程最佳实践 ✓